Bienvenidos a la Actividad 1, donde pondremos en práctica todo lo aprendido durante el bloque 2. Esta actividad la realizaremos en clase, se terminará en casa (debería completarse en clase) y se entregará el día 8 de octubre.
Vamos a poner en práctica cuatro aspectos del procesamiento de imágenes:
La finalidad es sencilla. Se os dará una imagen, a color, que tiene varias tonalidades y que está pintada con círculos.
La actividad consiste en contar el número de círculos de la imagen.
Se evaluará de la siguiente manera:
No se aceptará el formato .ipynb Habilitaré una actividad en Canvas para que podáis subir ambos archivos.
En primer lugar, cargamos todos los paquetes/frameworks que nos van a hacer falta. Se recomienda visitar la web: https://scikit-image.org/ para ver todas las funcionalidades que permite Scikit Image.
# Paquetes necesarios para la realización de esta práctica (no son necesarios conocerlos ni entenderlos por ahora)
from skimage.io import imread
from skimage import transform as tf
import matplotlib.pyplot as plt
# Cargamos la función para convertir de RGB a Escala de grises
from skimage.color import rgb2gray
# Paquete y funciones para realizar una umbralización con Scikit-image
from skimage.filters import threshold_otsu, threshold_local, threshold_niblack, threshold_sauvola
# Paquetes necesarios para la morfología matemática
from skimage.morphology import erosion, dilation, opening, closing
# Elementos estructurales
from skimage.morphology import disk, diamond, ball, rectangle
# Estas dos funciones nos sirven para detectar los objetos dentro de una imagen binaria
from skimage.morphology import label
from skimage.measure import regionprops
# Defino una función para mostrar una imagen por pantalla con el criterio que considero más acertado
def imshow(img, title):
fig, ax = plt.subplots(figsize=(7, 7))
# El comando que realmente muestra la imagen
ax.imshow(img,cmap=plt.cm.gray)
# Para evitar que aparezcan los números en los ejes
ax.set_xticks([]), ax.set_yticks([])
ax.set_title(title)
plt.show()
Lo primero de todo, vamos a leer la imagen. Recuerda que hay que subir la imagen cada vez que inicies sesión en el notebook y que la ruta se mira haciendo botón derecho sobre el archivo.
Con lo cual, aquí vamos a hacer dos cosas:
Hacemos esto para luego posteriormente umbralizar la imagen en escala de grises.
from google.colab import files
from PIL import Image
uploaded = files.upload() #subir imagen
imagen_pintura_puntos = list(uploaded.keys())[0] #damos un nombre al archivo
imagen = Image.open(imagen_pintura_puntos)
imagen.show()
Saving Pintura_Puntos.jpg to Pintura_Puntos (2).jpg
plt.imshow(imagen)
plt.axis('off') # Deshabilita los ejes
plt.show()
from skimage.color import rgb2gray
import matplotlib.pyplot as plt
# Convertir la imagen a escala de grises
imagen_gris = rgb2gray(imagen)
# Mostrar la imagen en escala de grises
plt.imshow(imagen_gris, cmap="gray")
plt.axis('off')
plt.show()
Se importa rg2gray de la biblioteca skimage.color y gracias a ella, podemos poner la imagen en escala de grises y, como ya hemos visto antes, empleamos la función plt. para mostrar la imagen.
# Os tendría que ir quedando una cosa así
Vamos a probar ahora diferentes métodos para umbralizar la imagen. Se pide en esta actividad:
UMBRALIZACIÓN OTSU
#Calcular el umbral de otsu
umbral_otsu = threshold_otsu(imagen_gris)
# Aplica el umbral de Otsu para binarizar la imagen
imagen_umbralizada_otsu = imagen_gris > umbral_otsu
# Muestra la imagen original y la umbralizada
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(imagen_gris, cmap='gray')
plt.axis ('off')
plt.title('Imagen Escala Grises')
plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada_otsu, cmap='gray')
plt.axis ('off')
plt.title('Imagen Umbralizada con Otsu')
plt.show()
threshold_otsu es una función que implementa el algoritmo de umbralización de Otsu. Por otro lado, imagen_gris es la imagen en escala de grises en la que deseas calcular el umbral de Otsu. Antes de usar la función, hemos convertido a escala de grises porque hace más sencilla la umbralización. Esto se debe a que solo se necesita un valor de intensidad por píxel en lugar de tres valores (R, G y B).
El umbral de Otsu divide la imagen en dos clases: píxeles que están por debajo del umbral y píxeles que están por encima.
Los píxeles por debajo del umbral se establecen en un valor (generalmente 0 o negro) y los píxeles por encima del umbral se establecen en otro valor (generalmente 255 o blanco) para separar objetos o características de interés en la imagen.
Por último he usado las funciones plt. para mostrar la imagen.
UMBRALIZACIÓN LOCAL
# Define el tamaño de la ventana local para calcular el umbral
block_size = 51
# Aplica el umbral local
umbralización_local = threshold_local(imagen_gris, block_size)
# Muestra la imagen original y la umbralizada
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(imagen_gris, cmap='gray')
plt.axis ('off')
plt.title('Imagen Escala Grises')
plt.subplot(1, 2, 2)
plt.imshow(imagen_gris > umbralización_local, cmap='gray')
plt.axis ('off')
plt.title('Imagen Umbralizada con Threshold Local')
plt.show()
En este código tenemos block_size = 51. Esto define el tamaño de la ventana local que se va a usar para calcular el umbral local. La ventana local es un área de la imagen en la que se calcula un umbral diferente para cada bloque de píxeles. En este caso, como block_size es 51 se calculará un umbral local basado en un bloque de 51x51 píxeles en la imagen.
imagen_umbralizada = threshold_local(imagen_gris, block_size): Esta línea aplica la umbralización local a la imagen en escala de grises imagen_gris. La umbralización local calcula umbrales diferentes para diferentes regiones de la imagen. Es una función útil cunado la iluminación es irregular o cuando deseas segmentar objetos de diferentes tamaños en la imagen. Cada píxel en imagen_umbralizada se asignará a 0 o 1 según si supera o no el umbral local en su vecindario correspondiente.
UMBRALIZACIÓN CON NIBLACK
# Establecer tamaño de la ventana
window_size1 = 47
# Aplica el umbral de Niblack
umbralización_niblack = threshold_niblack(imagen_gris, window_size)
# Muestra la imagen original y la umbralizada
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(imagen_gris, cmap='gray')
plt.axis('off')
plt.title('Imagen Escala Grises')
plt.subplot(1, 2, 2)
plt.imshow(imagen_gris > umbralización_niblack, cmap='gray')
plt.axis('off')
plt.title('Imagen Umbralizada con Threshold Niblack')
plt.show()
window_size = 47: define el tamaño de la ventana que se utilizará para calcular el umbral de Niblack. La ventana es un área cuadrada de la imagen que se desplaza por la imagen para calcular diferentes umbrales locales. En este caso es 47, lo que significa que se utilizará una ventana cuadrada de 47x47 píxeles.
imagen_umbralizada = threshold_niblack(imagen_gris, window_size=window_size) aplica el umbral de Niblack a la imagen en escala de grises.
Como en los casos anteriores, cada píxel se asignará a 0 o 1 según si supera o no el umbral de Niblack La umbralización de Niblack es útil en casos donde la iluminación es variable o desigual en diferentes partes de la imagen.
UMBRALIZACIÓN SAUVOLA
# Establecer tamaño de la ventana
window_size2=47
# Aplica el umbral de Sauvola
umbralización_sauvola = threshold_sauvola(imagen_gris, window_size)
# Muestra la imagen original y la umbralizada
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(imagen_gris, cmap='gray')
plt.axis('off')
plt.title('Imagen Escala Grises')
plt.subplot(1, 2, 2)
plt.imshow(imagen_gris > umbralización_sauvola, cmap='gray')
plt.axis('off')
plt.title('Imagen Umbralizada con Threshold Sauvola')
plt.show()
window_size: Es el tamaño de la ventana que se usa para calcular el umbral de Sauvola.
Cada píxel en imagen_umbralizada se asignará a 0 o 1 según si supera o no el umbral de Sauvola calculado en su vecindario correspondiente.
El umbral de Sauvola es un enfoque de umbralización local que considera tanto el valor medio de la intensidad en un vecindario como su desviación estándar local. Esto lo hace adecuado para imágenes con variaciones en la iluminación local, ya que adapta el umbral localmente en función de la información estadística en el área circundante de cada píxel.
Los métodos threshold_niblack y threshold_sauvola corresponden a la umbralización adaptativa. Ésta permite segmentar muy bien objetos de imagenes que tienen iluminación irregular o variaciones locales de contraste.
IMAGEN GIRADA
imagen_girada = np.rot90(np.rot90(imagen_gris))
plt.subplot(1, 2, 2)
plt.imshow(imagen_girada, cmap='gray')
plt.title('Imagen escala grises girada 180º')
plt.axis('off')
plt.show()
#Establecer tamaño del bloque
block_size2 = 47
# Calcular el umbral local utilizando threshold_local
umbralización_local = threshold_local(imagen_girada, block_size2)
# Aplica la umbralización local a la imagen
imagen_girada_umbralizada_local = imagen_girada > umbralización_local
# Mostrar la imagen en escala de grises girada y la imagen umbralizada local girada.
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.imshow(imagen_girada, cmap='gray')
plt.title('Imagen en Escala de Grises Girada')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(imagen_girada_umbralizada_local, cmap='gray')
plt.title('Imagen Umbralización Local Girada')
plt.axis('off')
plt.show()
Sí, se obtiene la misma imagen. Únicamente está girada 180º, pero la imagen no presenta otros cambios representativos.
De todos los métodos de umbralización usados, con el que mejor resultado se ha optenido es con threshold_local ya que es la que más se parece a la que decía el ejercicio que era el mejor resultado a alcanzar.
# Este es el mejor resultado que tendríais que alcanzar
Como se puede apreciar en la imagen hay varios elementos imperfectos:
Mediante el uso de morfología matemática (concretamente los cuatro operadores visto en clase) y los posibles elementos estructurales existentes, se pide:
EROSIÓN
import skimage.io
from skimage.morphology import erosion, dilation, opening, closing
from skimage import img_as_ubyte
kernel = np.ones((3, 3), np.uint8)
imagen_erosionada = erosion(imagen_gris > umbralización_local, kernel)
skimage.io.imshow(imagen_gris > umbralización_local)
skimage.io.imshow(imagen_erosionada)
skimage.io.show()
La erosión es una operación morfológica utilizada en el procesamiento de imágenes para reducir el tamaño de los objetos en una imagen binaria.
DILATACIÓN
kernel = np.ones((1, 2), np.uint8)
imagen_dilation = dilation(imagen_gris > umbralización_local, kernel)
skimage.io.imshow(imagen_gris > umbralización_local)
skimage.io.imshow(imagen_dilation)
skimage.io.show()
La dilatación es una operación morfológica utilizada en el procesamiento de imágenes para aumentar el tamaño de los objetos en una imagen binaria.
APERTURA
kernel = np.ones((1, 2), np.uint8)
imagen_opening = opening(imagen_gris > umbralización_local, kernel)
skimage.io.imshow(imagen_gris > umbralización_local)
skimage.io.imshow(imagen_opening)
skimage.io.show()
La función apertura es una operación morfológica usada en el procesamiento de imágenes que se suele utilizar para limpiar el ruido en las imágenes binarias, eliminar pequeños objetos no deseados y separar objetos que están muy cerca entre sí. Esta operación se realiaz haciendo primero una erosión y seguidamente una dilatación.
CLAUSURA
kernel = np.ones((1, 2), np.uint8)
imagen_clausura = closing(imagen_gris > umbralización_local, kernel)
skimage.io.imshow(imagen_gris > umbralización_local)
skimage.io.imshow(imagen_clausura)
skimage.io.show()
La operación de clausura se suele usar para cerrar pequeños agujeros en los objetos y para conectar componentes que pueden estar separados en la imagen binaria original. Esto puede ser útil para el procesamiento de imágenes y análisis de objetos.
RESULTADO
Elegiría como mejor imagen cualquiera de las tres últimas funciones (dilatación, apertura o clausura) haya que todas son prácticamente iguales. Descartaría la imagen obtenida con erosión ya que se ve demasiado oscura. Para el siguiente ejercicio he decidido seleccionar la apertura (aunque me hubiee valido cualquiera de los otros dos nombrado por lo ya explicado).
Haciendo uso de las funcionalidades cargadas al principio, se pide hacer una función que:
Por último, ¿qué se podría hacer para asegurar que no se tienen en cuenta posibles errores en la umbralización como pequeños puntos o posible ruido que haya llegado hasta este punto?
# Obtener la matriz de la imagen
matriz_imagen = np.array(imagen_opening)
print (matriz_imagen)
[[ True True True ... True True True] [ True True True ... False False False] [ True True True ... False False False] ... [ True True True ... True True True] [ True True True ... True True True] [ True True True ... True True True]]
# Obtener longitud de la imagen (cantidad de circulos)
len(matriz_imagen)
1930
# Mostrar imagen dilatada
plt.imshow(imagen_opening,'gray')
<matplotlib.image.AxesImage at 0x7955eb6498a0>
# Función para contar el número de círculos
def count_circles(imagen_binaria):
# Haz cosas
num_circulos = 'pongo esto solo para que veais cómo sería la estructura'
return num_circulos
Esta sección no es obligatoria pero la pongo para aquellos que quieran saber "¿y ahora qué se haría?".
Lo que hemos hecho hasta ahora es:
Es decir, tenemos varios parámetros y tenemos una función que nos dice cuál es el número de puntos dada una imagen. Variando dichos parámetros, variará también el número de puntos, pero no parece haber una relación directa.
También no hay que olvidar que desconocemos el número de puntos (nunca se ha dicho, aunque siempre puedes contarlos), por lo que no podemos seguir un proceso de aprendizaje supervisado (tipo descenso del gradiente sobre los parámetros anteriores para encontrar el mejor resultado).
Pero lo que sí podemos hacer es iterar el valor de los parámetros para alcanzar un máximo de puntos (asumiendo que dicho máximo corresponderá con el mejor resultado). Esto suele hacerse cuando no sabemos exáctamente el resultado que esperamos.
En definitiva, ahora se buscaría realizar un proceso iterativo para encontrar el valor máximo del número de puntos. Para ello haría falta:
product del paquete itertools).Podría decirse que esa combinación de parámetros es la mejor.